home *** CD-ROM | disk | FTP | other *** search
- Path: bloom-beacon.mit.edu!gatech!howland.reston.ans.net!pipex!uunet!library.erc.clarkson.edu!sun.soe.clarkson.edu!cline
- From: cline@sun.soe.clarkson.edu (Marshall Cline)
- Newsgroups: comp.lang.c++
- Subject: C++ FAQ: posting #2/4
- Followup-To: comp.lang.c++
- Date: 14 Nov 1994 00:26:25 GMT
- Organization: Paradigm Shift, Inc (training/consulting/OOD/OOP/C++)
- Lines: 834
- Sender: cline@sun.soe.clarkson.edu
- Distribution: world
- Expires: +1 month
- Message-ID: <3a6arh$gd7@library.erc.clarkson.edu>
- Reply-To: cline@parashift.com (Marshall Cline)
- NNTP-Posting-Host: sun.soe.clarkson.edu
- Summary: Please read this before posting to comp.lang.c++
-
- comp.lang.c++ Frequently Asked Questions list (with answers, fortunately).
- Copyright (C) 1991-94 Marshall P. Cline, Ph.D.
- Posting 2 of 4.
- Posting #1 explains copying permissions, (no)warranty, table-of-contents, etc
-
- ==============================================================================
- SECTION 9: Freestore management
- ==============================================================================
-
- Q30: Does "delete p" delete the pointer "p", or the pointed-to-data, "*p"?
-
- The pointed-to-data.
-
- "delete" really means "delete the thing pointed to by." The same abuse of
- English occurs when "free"ing the memory pointed to by a ptr in C ("free(p)"
- really means "free_the_stuff_pointed_to_by(p)").
-
- ==============================================================================
-
- Q31: Can I "free()" pointers allocated with "new"? Can I "delete" pointers
- alloc'd with "malloc()"?
-
- No.
-
- It is perfectly legal, moral, and wholesome to use malloc/free and new/delete
- in the same program, but it is illegal, immoral, and despicable to free a
- pointer allocated via new, or to delete a pointer allocated via malloc.
-
- ==============================================================================
-
- Q32: Why should I use "new" instead of trustworthy old malloc()?
-
- Constructors/destructors, type safety, overridability.
-
- Constructors/destructors: unlike "malloc(sizeof(Fred))", "new Fred()" calls
- Fred's constructor. Similarly, "delete p" calls "*p"'s destructor.
-
- Type safety: malloc() returns a "void*" which isn't type safe. "new Fred()"
- returns a ptr of the right type (a "Fred*").
-
- Overridability: "new" is an operator that can be overridden by a class, while
- "malloc" is not overridable on a per-class basis.
-
- ==============================================================================
-
- Q33: Why doesn't C++ have a "realloc()" along with "new" and "delete"?
-
- To save you from disaster.
-
- When realloc() has to copy the allocation, it uses a BITWISE copy operation,
- which will tear most C++ objects to shreds. C++ objects should be allowed to
- copy themselves: they use their own copy constructor or assignment operator.
-
- ==============================================================================
-
- Q34: How do I allocate / unallocate an array of things?
-
- Use new[] and delete[]:
-
- Fred* p = new Fred[100];
- //...
- delete [] p;
-
- Any time you use the "[...]" in the "new" expression, you *!*MUST*!* use "[]"
- in the "delete" statement. This syntax is necessary because there is no
- syntactic difference between a pointer to a thing and a pointer to an array of
- things (something we inherited from C).
-
- ==============================================================================
-
- Q35: What if I forget the "[]" when "delete"ing array allocated via "new
- Fred[n]"?
-
- All life comes to a catastrophic end.
-
- It is the programmer's --not the compiler's-- responsibility to get the
- connection between new[] and delete[] correct. If you get it wrong, neither a
- compile-time nor a run-time error message will be generated by the compiler.
- Heap corruption is a likely result. Or worse. Your program will probably die.
-
- ==============================================================================
- SECTION 10: Debugging and error handling
- ==============================================================================
-
- Q36: How can I handle a constructor that fails?
-
- Throw an exception.
-
- Constructors don't have a return type, so it's not possible to use error codes.
- The best way to signal constructor failure is therefore to throw an exception.
-
- Before C++ had exceptions, we signaled constructor failure by putting the
- object into a "half baked" state (e.g., by setting an internal status bit).
- There was a query ("inspector") method to check this bit, that allowed clients
- to discover whether they had a live object. Other member functions would also
- check this bit, and, if the object wasn't really alive, do a no-op (or perhaps
- something more obnoxious such as "abort()"). This was really ugly.
-
- ==============================================================================
-
- Q37: How should I handle resources if my constructors may throw exceptions?
-
- Every data member inside your object should clean up its own mess.
-
- If a constructor throws an exception, the object's destructor is NOT run. If
- your object has already done something that needs to be undone (such as
- allocating some memory, opening a file, or locking a semaphore), this "stuff
- that needs to be undone" MUST be remembered by a data member inside the object.
-
- For example, rather than allocating memory into a raw "Fred*" data member, put
- the allocated memory into a "smart pointer" member object, and the destructor
- of this smart pointer will delete the Fred object when the smart pointer dies.
-
- ==============================================================================
- SECTION 11: Const correctness
- ==============================================================================
-
- Q38: What is "const correctness"?
-
- A good thing.
-
- Const correctness uses the keyword "const" to ensure const objects don't get
- mutated. E.g., if function "f()" accepts a "String", and "f()" wants to
- promise not to change the "String", you:
-
- * can either pass by value: void f( String s ) { /*...*/ }
- * or by constant reference: void f(const String& s ) { /*...*/ }
- * or by constant pointer: void f(const String* sptr) { /*...*/ }
- * but NOT by non-const ref: void f( String& s ) { /*...*/ }
- * NOR by non-const pointer: void f( String* sptr) { /*...*/ }
-
- Attempted changes to "s" within a fn that takes a "const String&" are flagged
- as compile-time errors; neither run-time space nor speed is degraded.
-
- Declaring the "constness" of a parameter is just another form of type safety.
- It is almost as if a constant String, for example, "lost" its various mutative
- operations. If you find type safety helps you get systems correct (it does;
- especially in large systems), you'll find const correctness helps also.
-
- ==============================================================================
-
- Q39: Should I try to get things const correct "sooner" or "later"?
-
- At the very, very, VERY beginning.
-
- Back-patching const correctness results in a snowball effect: every "const" you
- add "over here" requires four more to be added "over there."
-
- ==============================================================================
-
- Q40: What is a "const member function"?
-
- A member function that inspects (rather than mutates) its object.
-
- class Fred {
- public:
- void f() const;
- }; // ^^^^^--- this implies "fred.f()" won't change "fred"
-
- This means that the ABSTRACT (client-visible) state of the object isn't going
- to change (as opposed to promising that the "raw bits of the object's struct
- aren't going to change). C++ compilers aren't allowed to take the "bitwise"
- interpretation, since a non-const alias could exist which could modify the
- state of the object (gluing a "const" ptr to an object doesn't promise the
- object won't change; it only promises that the object won't change VIA THAT
- POINTER).
-
- "const" member functions are often called "inspectors." Non-"const" member
- functions are often called "mutators."
-
- ==============================================================================
-
- Q41: What do I do if I want to update an "invisible" data member inside a
- "const" member function?
-
- Use "mutable", or use "const_cast".
-
- A small percentage of inspectors need to make innocuous changes to data members
- (e.g., a "Set" object might want to cache its last lookup in hopes of improving
- the performance of its next lookup). By saying the changes are "inocuous," I
- mean that the changes wouldn't be visible from outside the object's interface
- (otherwise the method would be a mutator rather than an inspector).
-
- When this happens, the data member which will be modified should be marked as
- "mutable" (put the "mutable" keyword just before the data member's declaration;
- i.e., in the same place where you could put "const"). This tells the compiler
- that the data member is allowed to change during a const member function. If
- you can't use "mutable", you can cast away the constness of "this" via
- "const_cast". E.g., in "Set::lookup() const", you might say,
-
- Set* self = const_cast<Set*>(this);
-
- After this line, "self" will have the same bits as "this" (e.g., "self==this"),
- but "self" is a "Set*" rather than a "const Set*". Therefore you can use
- "self" to modify the object pointed to by "this".
-
- ==============================================================================
-
- Q42: Does "const_cast" mean lost optimization opportunities?
-
- In theory, yes; in practice, no.
-
- Even if a compiler outlawed "const_cast", the only way to avoid flushing the
- register cache across a "const" member function call would be to ensure that
- there are no non-const pointers that alias the object. This can only happen in
- rare cases (when the object is constructed in the scope of the const member fn
- invocation, and when all the non-const member function invocations between the
- object's construction and the const member fn invocation are statically bound,
- and when every one of these invocations is also "inline"d, and when the
- constructor itself is "inline"d, and when any member fns the constructor calls
- are inline).
-
- ==============================================================================
- SECTION 12: Inheritance
- ==============================================================================
-
- Q43: Is inheritance important to C++?
-
- Yep.
-
- Inheritance is what separates abstract data type (ADT) programming from OOP.
-
- ==============================================================================
-
- Q44: When would I use inheritance?
-
- As a specification device.
-
- Human beings abstract things on two dimensions: part-of and kind-of. A Ford
- Taurus is-a-kind-of-a Car, and a Ford Taurus has-a Engine, Tires, etc. The
- part-of hierarchy has been a part of software since the ADT style became
- relevant; inheritance adds "the other" major dimension of decomposition.
-
- ==============================================================================
-
- Q45: How do you express inheritance in C++?
-
- By the ": public" syntax:
-
- class Car : public Vehicle {
- //^^^^^^^^---- ": public" is pronounced "is-a-kind-of-a'
- //...
- };
-
- We state the above relationship in several ways:
- * Car is "a kind of a" Vehicle
- * Car is "derived from" Vehicle
- * Car is "a specialized" Vehicle
- * Car is the "subclass" of Vehicle
- * Vehicle is the "base class" of Car
- * Vehicle is the "superclass" of Car (this not as common in the C++ community)
-
- ==============================================================================
-
- Q46: Is it ok to convert a pointer from a derived class to its base class?
-
- Yes.
-
- A derived class is a specialized version of the base class ("Derived is a
- kind-of Base"). The upward conversion is perfectly safe, and happens all the
- time (if I am pointing at a car, I am in fact pointing at a vehicle):
-
- void f(Vehicle* v);
- void g(Car* c) { f(c); } //perfectly safe; no cast
-
- Note that the answer to this FAQ assumes we're talking about "public"
- inheritance; see below on "private/protected" inheritance for "the other kind".
-
- ==============================================================================
-
- Q47: Derived* --> Base* works ok; why doesn't Derived** --> Base** work?
-
- C++ allows a Derived* to be converted to a Base*, since a Derived object is a
- kind of a Base object. However trying to convert a Derived** to a Base** is
- (correctly) flagged as an error (if it was allowed, the Base** could be
- dereferenced (yielding a Base*), and the Base* could be made to point to an
- object of a DIFFERENT derived class. This would be an error.
-
- As a corollary, an array of Deriveds is-NOT-a-kind-of array of Bases. At
- Paradigm Shift, Inc. we use the following example in our C++ training sessions:
-
- "A bag of apples is NOT a bag of fruit".
-
- If a bag of apples COULD be passed as a bag of fruit, someone could put a
- banana into the bag of apples!
-
- ==============================================================================
-
- Q48: Does array-of-Derived is-NOT-a-kind-of array-of-Base mean arrays are
- bad?
-
- Yes, "arrays are evil" (jest kidd'n :-).
-
- There's a very subtle problem with using raw built-in arrays. Consider this:
-
- void f(Base* arrayOfBase)
- {
- arrayOfBase[3].memberfn();
- }
-
- main()
- {
- Derived arrayOfDerived[10];
- f(arrayOfDerived);
- }
-
- The compiler thinks this is perfectly type-safe, since it can convert a
- Derived* to a Base*. But in reality it is horrendously evil: since Derived
- might be larger than Base, the array index in f() not only isn't type safe, it
- may not even be pointing at a real object! In general it'll be pointing
- somewhere into the innards of some poor Derived.
-
- The root problem is that C++ can't distinguish between a ptr-to-a-thing and a
- ptr-to-an-array-of-things. Naturally C++ "inherited" this feature from C.
-
- NOTE: if we had used an array-like CLASS instead of using a raw array (e.g., an
- "Array<T>" rather than a "T[]"), this problem would have been properly trapped
- as an error at compile time rather than at run-time.
-
- ==============================================================================
- SUBSECTION 12A: Inheritance -- Virtual functions
- ==============================================================================
-
- Q49: What is a "virtual member function"?
-
- A virtual function allows derived classes to replace the implementation
- provided by the base class. The compiler ensures the replacement is always
- called whenever the object in question is actually of the derived class, even
- if the object is accessed by a base pointer rather than a derived pointer.
- This allows algorithms in the base class to be replaced in the derived class,
- even if users don't know about the derived class.
-
- Note: the derived class can partially replace ("override") the base class
- method (the derived class method can invoke the base class version if desired).
-
- ==============================================================================
-
- Q50: How can C++ achieve dynamic binding yet also static typing?
-
- In the following discussion, "ptr" means either a pointer or a reference.
-
- When you have a ptr, there are two types: the (static) type of the ptr, and the
- (dynamic) type of the pointed-to object (the object may actually be of a class
- that is derived from the class of the ptr).
-
- "Static typing" means that the "legality" of the call is checked based on the
- static type of the ptr: if the type of the ptr can handle the member fn,
- certainly the pointed-to object can handle it as well.
-
- "Dynamic binding" means that the "code" that is called is based on the dynamic
- type of the pointed-to object. This is called "dynamic binding," since the
- actual code being called is determined dynamically (at run time).
-
- ==============================================================================
-
- Q51: Should a derived class replace ("override") a non-virtual fn from a base
- class?
-
- It's legal, but it ain't moral.
-
- Experienced C++ programmers will sometimes redefine a non-virtual fn for
- efficiency (the alternate implementation might make better use of the derived
- class' resources), or to get around the hiding rule (see below, and ARM
- sect.13.1). However the client-visible effects must be IDENTICAL, since
- non-virtual fns are dispatched based on the static type of the ptr/ref rather
- than the dynamic type of the pointed-to/referenced object.
-
- ==============================================================================
-
- Q52: What's the meaning of, "Warning: Derived::f(int) hides Base::f(float)"?
-
- It means you're going to die.
-
- Here's the mess you're in: if Derived declares a member function named "f", and
- Base declares a member function named "f" with a different signature (e.g.,
- different parameter types and/or constness), then the Base "f" is "hidden"
- rather than "overloaded" or "overridden" (even if the Base "f" is virtual).
-
- Here's how you get out of the mess: Derived must redefine the Base member
- function(s) that are hidden (even if they are non-virtual). Normally this
- re-definition merely calls the appropriate Base member function. E.g.,
-
- class Base {
- public:
- void f(int);
- };
-
- class Derived : public Base {
- public:
- void f(double);
- void f(int i) { Base::f(i); }
- }; // ^^^^^^^^^^--- redefinition merely calls Base::f(int)
-
- ==============================================================================
- SUBSECTION 12B: Inheritance -- Conformance
- ==============================================================================
-
- Q53: Should I hide public member fns inherited from my base class?
-
- Never, never, never do this. Never. NEVER!
-
- Attempting to hide (eliminate, revoke) inherited public member functions is an
- all-too-common design error. It usually stems from muddy thinking.
-
- ==============================================================================
-
- Q54: Is a "Circle" a kind-of an "Ellipse"?
-
- Not if Ellipse promises to be able to change its size assymetrically.
-
- For example, suppose Ellipse has a "setSize(x,y)" method, and suppose this
- method promises "the Ellipse's width() will be x, and its height() will be y".
- In this case, Circle can't be a kind-of Ellipse. Simply put, if Ellipse can do
- something Circle can't, then Circle can't be a kind of Ellipse.
-
- This leaves two potential (valid) relationships between Circle and Ellipse:
- * Make Circle and Ellipse completely unrelated classes.
- * Derive Circle and Ellipse from a base class representing "Ellipses that
- can't NECESSARILY perform an unequal-setSize operation."
-
- In the first case, Ellipse could be derived from class "AsymmetricShape" (with
- setSize(x,y) being introduced in AsymmetricShape), and Circle could be derived
- from "SymmetricShape," which has a setSize(size) member fn.
-
- In the second case, class "Oval" could only have "setSize(size)" which sets
- both the "width()" and the "height()" to "size", then derive both Ellipse and
- Circle from Oval. Ellipse --but not Circle-- adds the "setSize(x,y)" operation
- (see the "hiding rule" for a caveat if the same method name "setSize()" is used
- for both operations).
-
- ==============================================================================
-
- Q55: Are there other options to the "Circle is/isnot kind-of Ellipse"
- dilemma?
-
- If you claim that all Ellipses can be squashed assymetrically, and you claim
- that Circle is a kind-of Ellipse, and you claim that Circle can't be squashed
- assymetrically, clearly you've got to adjust (revoke, actually) one of your
- claims. Thus you've either got to get rid of "Ellipse::setSize(x,y)", get rid
- of the inheritance relationship between Circle and Ellipse, or admit that your
- "Circle"s aren't necessarily circular.
-
- Here are the two most common traps new OO/C++ programmers regularly fall into.
- They attempt to use coding hacks to cover up a broken design (they redefine
- Circle::setSize(x,y) to throw an exception, call "abort()", or choose the
- average of the two parameters, or to be a no-op). Unfortunately all these
- hacks will surprise users, since users are expecting "width() == x" and
- "height() == y".
-
- The only rational way out of this would be to weaken the promise made by
- Ellipse's "setSize(x,y)" (e.g., you'd have to change it to, "This method MIGHT
- set width() to x and height() to y, or it might do NOTHING"). Unfortunately
- this dilutes the contract into dribble, since the user can't rely on any
- meaningful behavior. The whole hierarchy therefore begins to be worthless
- (it's hard to convince someone to use an object if you have to shrug your
- shoulders when asked what the object does for them).
-
- ==============================================================================
- SUBSECTION 12C: Inheritance -- Access rules
- ==============================================================================
-
- Q56: Why can't my derived class access "private" things from my base class?
-
- To protect you from future changes to the base class.
-
- Derived classes do not get access to private members of a base class. This
- effectively "seals off" the derived class from any changes made to the private
- members of the base class.
-
- ==============================================================================
-
- Q57: What's the difference between "public:", "private:", and "protected:"?
-
- "Private:" is discussed in the previous section, and "public:" means "anyone
- can access it." The third option, "protected:", makes a member (either data
- member or member fn) accessible to subclasses.
-
- ==============================================================================
-
- Q58: How can I protect subclasses from breaking when I change internal parts?
-
- A class has two distinct interfaces for two distinct sets of clients:
- * its "public:" interface serves unrelated classes.
- * its "protected:" interface serves derived classes.
-
- Unless you expect all your subclasses to be built by your own team, you should
- consider making your base class's bits be "private:", and use "protected:"
- inline access functions to access these data. This way the private bits can
- change, but the derived class's code won't break unless you change the
- protected access functions.
-
- ==============================================================================
- SUBSECTION 12D: Inheritance -- Constructors and destructors
- ==============================================================================
-
- Q59: When my base class's constructor calls a virtual function, why doesn't my
- derived class's override of that virtual function get invoked?
-
- During the Base class's constructor, the object isn't yet a Derived, so if
- "Base::Base()" calls a virtual function "virt()", the "Base::virt()" will be
- invoked, even if "Derived::virt()" exists.
-
- Similarly, during Base's destructor, the object is no longer a Derived, so when
- Base::~Base() calls "virt()", "Base::virt()" gets control, NOT the
- "Derived::virt()" override.
-
- You'll quickly see the wisdom of this approach when you imagine the disaster if
- "Derived::virt()" touched a member object from the Derived class.
-
- ==============================================================================
-
- Q60: Does a derived class destructor need to explicitly call the base
- destructor?
-
- No, never explicitly call a destructor (where "never" means "rarely").
-
- A derived class's destructor (whether or not you explicitly define one)
- AUTOMATICALLY invokes the destructors for member objects and base class
- subobjects. Member objects are destroyed in the reverse order they appear
- within the class, then base class subobjects are destroyed in the reverse order
- that they appear in the class's list of base classes.
-
- You should ONLY explicitly call a destructor in esoteric situations, such as
- when destroying an object created by the "placement new operator."
-
- ==============================================================================
- SUBSECTION 12E: Inheritance -- Private and protected inheritance
- ==============================================================================
-
- Q61: How do you express "private inheritance"?
-
- When you use ": private" instead of ": public." E.g.,
-
- class Foo : private Bar {
- //...
- };
-
- ==============================================================================
-
- Q62: How are "private inheritance" and "composition" similar?
-
- Private inheritance is a syntactic variant of composition (has-a).
-
- E.g., the "car has-a engine" relationship can be expressed using composition:
-
- class Engine {
- public:
- Engine(int numCylinders);
- void start(); //starts this Engine
- };
-
- class Car {
- public:
- Car() : e_(8) { } //initializes this Car with 8 cylinders
- void start() { e_.start(); } //start this Car by starting its engine
- private:
- Engine e_;
- };
-
- The same "has-a" relationship can also be expressed using private inheritance:
-
- class Car : private Engine {
- public:
- Car() : Engine(8) { } //initializes this Car with 8 cylinders
- Engine::start; //start this Car by starting its engine
- };
-
- There are several similarities between these two forms of composition:
- * in both cases there is exactly one Engine member object contained in a Car.
- * in neither case can users (outsiders) convert a Car* to an Engine*.
-
- There are also several distinctions:
- * the second form is needed if you want to contain several Engines per Car.
- * the first form can introduce unnecessary multiple inheritance.
- * the first form allows members of Car to convert a Car* to an Engine*.
- * the first form allows access to the "protected" members of the base class.
- * the first form allows Car to override Engine's virtual functions.
-
- Note that private inheritance is usually used to gain access into the
- "protected:" members of the base class, but this is usually a short-term
- solution (translation: a band-aid; see below).
-
- ==============================================================================
-
- Q63: Which should I prefer: composition or private inheritance?
-
- Composition.
-
- Normally you don't WANT to have access to the internals of too many other
- classes, and private inheritance gives you some of this extra power (and
- responsibility). But private inheritance isn't evil; it's just more expensive
- to maintain, since it increases the probability that someone will change
- something that will break your code.
-
- A legitimate, long-term use for private inheritance is when you want to build a
- class Fred that uses code in a class Wilma, and the code from class Wilma needs
- to invoke methods from your new class, Fred. In this case, Fred calls
- non-virtuals in Wilma, and Wilma calls (usually pure) virtuals in itself, which
- are overridden by Fred. This would be much harder to do with composition.
-
- class Wilma {
- protected:
- void fredCallsWilma()
- { cout << "Wilma::fredCallsWilma()\n"; wilmaCallsFred(); }
- virtual void wilmaCallsFred() = 0;
- };
-
- class Fred : private Wilma {
- public:
- void barney()
- { cout << "Fred::barney()\n"; Wilma::fredCallsWilma(); }
- protected:
- virtual void wilmaCallsFred()
- { cout << "Fred::wilmaCallsFred()\n"; }
- };
-
- ==============================================================================
-
- Q64: Should I pointer-cast from a "privately" derived class to its base
- class?
-
- Generally, No.
-
- From a method or friend of a privately derived class, the relationship to the
- base class is known, and the upward conversion from PrivatelyDer* to Base* (or
- PrivatelyDer& to Base&) is safe; no cast is needed or recommended.
-
- However users of PrivateDer should avoid this unsafe conversion, since it is
- based on a "private" decision of PrivateDer, and is subject to change without
- notice.
-
- ==============================================================================
-
- Q65: How is protected inheritance related to private inheritance?
-
- Similarities: both allow overriding virtuals in the private/protected base
- class, neither claims the derived is a kind-of its base.
-
- Dissimilarities: protected inheritance allows derived classes of derived
- classes to know about the inheritance relationship (it exposes your grand kids
- to your implementation details). This has both benefits (it allows subclasses
- of the protected derived class to exploit the relationship to the protected
- base class) and costs (the protected derived class can't change the
- relationship without potentially breaking further derived classes).
-
- Protected inheritance uses the ": protected" syntax:
-
- class Car : protected Engine {
- //...
- };
-
- ==============================================================================
-
- Q66: What are the access rules with "private" and "protected" inheritance?
-
- Take these classes as examples:
-
- class B { /*...*/ };
- class D_priv : private B { /*...*/ };
- class D_prot : protected B { /*...*/ };
- class D_publ : public B { /*...*/ };
- class UserClass { B b; /*...*/ };
-
- None of the subclasses can access anything that is private in B. In D_priv,
- the public and protected parts of B are "private". In D_prot, the public and
- protected parts of B are "protected". In D_publ, the public parts of B are
- public and the protected parts of B are protected (D_publ is-a-kind-of-a B).
- Class "UserClass" can only access the public parts of B, which "seals off"
- UserClass from B.
-
- To make a public member of B so it is public in D_priv or D_prot, state the
- name of the member with a "B::" prefix. E.g., to make member "B::f(int,float)"
- public in D_prot, you would say:
-
- class D_prot : protected B {
- public:
- B::f; //note: not "B::f(int,float)"
- };
-
- ==============================================================================
- SECTION 13: Abstraction
- ==============================================================================
-
- Q67: What's the big deal of separating interface from implementation?
-
- Interfaces are a company's most valuable resources. Designing an interface
- takes longer than whipping together a concrete class which fulfills that
- interface. Furthermore interfaces require the time of more expensive people.
-
- Since interfaces are so valuable, they should be protected from being tarnished
- by data structures and other implementation artifacts. Thus you should
- separate interface from implementation.
-
- ==============================================================================
-
- Q68: How do I separate interface from implementation in C++ (like Modula-2)?
-
- Use an ABC (see next FAQ).
-
- ==============================================================================
-
- Q69: What is an ABC ("abstract base class")?
-
- At the design level, an ABC corresponds to an abstract concept. If you asked a
- Mechanic if he repaired Vehicles, he'd probably wonder what KIND-OF Vehicle you
- had in mind. Chances are he doesn't repair space shuttles, ocean liners,
- bicycles, or nuclear submarines. The problem is that the term "Vehicle" is an
- abstract concept (e.g., you can't build a "vehicle" unless you know what kind
- of vehicle to build). In C++, class Vehicle would be an ABC, with Bicycle,
- SpaceShuttle, etc, being subclasses (an OceanLiner is-a-kind-of-a Vehicle). In
- real-world OOP, ABCs show up all over the place.
-
- As programming language level, an ABC is a class that has one or more pure
- virtual member functions (see next FAQ). You cannot make an object (instance)
- of an ABC.
-
- ==============================================================================
-
- Q70: What is a "pure virtual" member function?
-
- A member function of an ABC that you can only implement in a derived class.
-
- Some member functions exist in concept, but can't have any actual defn. E.g.,
- suppose I asked you to draw a Shape at location (x,y) that has size 7. You'd
- ask me "what kind of shape should I draw?" (circles, squares, hexagons, etc,
- are drawn differently). In C++, we indicate the existence of the "draw()'
- method, but we recognize it can only be defined in subclasses:
-
- class Shape {
- public:
- virtual void draw() const = 0;
- //... ^^^--- "= 0" means it is "pure virtual"
- };
-
- This pure virtual function makes "Shape" an ABC. If you want, you can think of
- the "= 0" syntax as if if the code were at the NULL pointer. Thus "Shape"
- promises a service to its users, yet Shape isn't able to provide any code to
- fulfill that promise. This ensures any actual object created from a [concrete]
- class derived of Shape *WILL* have the indicated member fn, even though the
- base class doesn't have enough information to actually DEFINE it yet.
-
- ==============================================================================
-
- Q71: How can I provide printing for an entire hierarchy of classes?
-
- Provide a friend operator<< that calls a protected virtual function:
-
- class Base {
- public:
- friend ostream& operator<< (ostream& o, const Base& b)
- { b.print(o); return o; }
- //...
- protected:
- virtual void print(ostream& o) const; //or "=0;" if "Base" is an ABC
- };
-
- class Derived : public Base {
- protected:
- virtual void print(ostream& o) const;
- };
-
- Now all subclasses of Base merely provide their own "print(ostream&) const"
- member function (they all share the common "<<" operator). This technique
- allows friends to ACT as if they supported dynamic binding.
-
- ==============================================================================
-
- Q72: When should my destructor be virtual?
-
- When you may "delete" a derived object via a base pointer.
-
- Virtual fns bind to the code associated with the class of the object, rather
- than with the class of the pointer/ref. When you say "delete basePtr", and the
- base class has a virtual destructor, the destructor that gets invoked is the
- one associated with the type of the object *basePtr, rather than the one
- associated with the type of the pointer. This is generally A Good Thing.
-
- To make life easy for you, the only time you wouldn't want to make a class's
- destructor virtual is if that class has NO virtual fns, since the introduction
- of the first virtual fn imposes some space overhead in each object (typically
- one machine word). This is how the compiler implements the magic of dynamic
- binding; it usually boils down to an extra ptr per object called the "virtual
- table pointer" or "vptr".
-
- ==============================================================================
-
- Q73: What is a "virtual constructor"?
-
- An idiom that allows you to do something that C++ doesn't directly support.
-
- You can get the effect of virtual constructor by a virtual "createCopy()"
- member fn (for copy constructing), or a virtual "createSimilar()" member fn
- (for the default constructor).
-
- class Shape {
- public:
- virtual ~Shape() { } //see on "virtual destructors" for more
- virtual void draw() = 0;
- virtual void move() = 0;
- //...
- virtual Shape* createCopy() const = 0;
- virtual Shape* createSimilar() const = 0;
- };
-
- class Circle : public Shape {
- public:
- Circle* createCopy() const { return new Circle(*this); }
- Circle* createSimilar() const { return new Circle(); }
- //...
- };
-
- The invocation of "Circle(*this)" is that of copy construction ("*this" has
- type "const Circle&" in these methods). "createSimilar()" is similar, but it
- constructs a "default" Circle.
-
- Users use these as if they were "virtual constructors":
-
- void userCode(Shape& s)
- {
- Shape* s2 = s.createCopy();
- Shape* s3 = s.createSimilar();
- //...
- delete s2; //relies on destructor being virtual!!
- delete s3; // ditto
- }
-
- This fn will work correctly regardless of whether the Shape is a Circle,
- Square, or some other kind-of Shape that doesn't even exist yet.
-
- --
- Marshall Cline
- --
- Marshall P. Cline, Ph.D. / Paradigm Shift Inc / PO Box 5108 / Potsdam NY 13676
- cline@sun.soe.clarkson.edu / 315-353-6100 / FAX: 315-353-6110
-